/**
 *  Axios 实例, ajax请求底层方法
 *  官方文档 [https://github.com/axios/axios]{@link https://github.com/axios/axios}
 *  @module utils/axios
 */
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';

import qs from 'qs';

// vuex数据
import user from '@/store/user';

import { compile } from 'path-to-regexp';

import { getHost } from './url';

// AJAX_SUCCESS
import { AJAX_SUCCESS } from '@/config';
import type { LoadingInstance } from 'element-plus';

// 请求错误自定义
const errorCode = {
	401: '认证失败，无法访问系统资源',
	403: '当前操作没有权限',
	404: '访问资源不存在',
	512: '许可证异常',
	513: '许可证宿主机异常',
	514: '许可证过期',
	515: '许可证服务异常',
	default: '系统未知错误,请反馈给管理员'
};

/**
 * 白名单
 */
const whiteList = [
	'/auth/login',
	'/auth/logout',
	'/captcha/image',
	'/system/user/profile/info',
	'/home/routers',
	{ pattern: /^\/system\/dict\/*/ } // 支持正则表达式
];

function whiteRouter(url: string) {
	return whiteList.some((item) => {
		if (typeof item === 'object' && item.pattern) return item.pattern.test(url);
		return url === item;
	});
}

/**
 * 判断是否为JSON格式
 * @param {*} str
 * @returns
 */
function isJSON(str: string | objType) {
	if (typeof str !== 'string') return false;

	try {
		JSON.parse(str);
		return true;
	} catch {
		return false;
	}
}

/**
 * 创建缓存key, 由请求url、类型、参数、发送数据构成的标识符
 * @param {string} url 请求url
 * @param {string} type 请求类型
 * @param {object} params url参数对象
 * @param {object} data 请求数据
 * @return {string}
 */
function createKey(config: Partial<InternalAxiosRequestConfig>) {
	const { url, method, params = {}, data = {} } = config;
	return encodeURIComponent([url, method, isJSON(params) ? params : JSON.stringify(params), isJSON(data) ? data : JSON.stringify(data)].join(','));
}

// 编译过的url缓存
const pathToRegexCaches = new Map(),
	httpPromiseArr = new Map();

// 全局loading
let loadingInstance: LoadingInstance;

/**
 * 请求系统错误时 取消所有正在进行的请求函数
 * @returns
 */
export function cancelFn() {
	for (const item of httpPromiseArr.values()) {
		if (isFunction(item.cancel)) item.cancel('取消请求');
	}
	httpPromiseArr.clear();
	// 关闭全局loading
	loadingInstance?.close();
}

/**
 * Axios实例化参数选项对象
 * @const
 * @type {object}
 * @property {object} headers 请求头对象对象，默认：null
 * @property {number} timeout 超时时间，默认：0， 不限制
 * @property {boolean} withCredentials 是否带上验证信息， 默认：true
 * @property {number} maxContentLength 限制最大发送内容长度，默认：-1 不限制
 * @property {strin} baseURL 默认请求拼接的前缀
 */
export const service = axios.create({
	baseURL: import.meta.env.VITE_APP_BASE_API,
	headers: {
		'Content-Type': 'application/json;charset=UTF-8',
		// ie浏览器get请求兼容问题
		'Cache-Control': 'no-cache',
		'If-Modified-Since': '0'
	},
	timeout: 0,
	withCredentials: false,
	responseType: 'json',
	maxContentLength: -1,
	paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'comma' })
});

/**
 * 在请求发送数据之前，对发送数据进行转换
 */
function requestSuccess(config: InternalAxiosRequestConfig) {
	// 是否需要设置 token
	const isToken = (config.headers || {}).isToken === false,
		{ token, uid } = user();
	if (token && !isToken) {
		config.headers!['Authorization'] = 'Bearer ' + token; // 让每个请求携带自定义token
	}

	// 创建全局loading，排除不需要loading的接口
	if (httpPromiseArr.size === 1 && !whiteRouter(config.url!)) {
		loadingInstance = ElLoading.service({
			text: '加载中...',
			target: '#ContentArea'
		});
	}

	// 联合登陆携带uid
	if (uid) config.headers!['uid'] = uid;

	/**
	 * 请求未完成时保存取消的cancelToken，缓存正在进行的请求
	 */
	const key = createKey(config);
	config.cancelToken = new axios.CancelToken((cancel) => {
		if (httpPromiseArr.get(key)) httpPromiseArr.get(key).cancel = cancel;
	});

	return config;
}
function requestError(error: AxiosError) {
	ElMessage.error({ message: '加载超时' });
	return Promise.reject(error);
}
service.interceptors.request.use(requestSuccess, requestError);

/**
 * 响应拦截器
 * @param {object} res 响应对象
 * @param {object} res.data 响应数据
 * @param {number|string} res.data.code 响应状态码
 * @param {string} res.data.msg 响应信息
 * @param {object} res.data.data 响应数据
 */
function responseSuccess(res: AxiosResponse<ApiResponse>) {
	// 未设置状态码则默认成功状态
	const code = res?.data?.code || AJAX_SUCCESS;
	// 获取错误信息
	const msg = errorCode[code as keyof typeof errorCode] || res?.data?.msg || errorCode['default'];

	// 请求成功后从正在进行的请求数组中删除
	const key = createKey(res.config);
	let repeatArr = [];
	if (httpPromiseArr.has(key)) {
		repeatArr = httpPromiseArr.get(key).repeatArr;
		httpPromiseArr.delete(key);
	}
	// 全部请求结束关闭loading
	if (httpPromiseArr.size === 0) {
		nextTick(() => {
			loadingInstance?.close();
		});
	}

	if ([401, 418].includes(Number(code))) {
		if (code === 418) {
			(user() as any).LogOutSET();
			$router.replace('/login');
		}
		if (code === 401) {
			user()
				//@ts-ignore
				.LogOut()
				.then(() => {
					$router.replace({
						path: '/login',
						query: {
							redirect: useRoute().fullPath
						}
					});
				});
		}
		cancelFn();
		ElMessage.error(msg);
		return Promise.reject(res.data);
	} else if (code === 512 || code === 513 || code === 514 || code === 515) {
		$router.replace({ path: '/512' });
		ElMessage.error(msg);
		$store.guarder.pageInfoError = msg;
	} else if (code !== 200) {
		ElMessage.error(msg);
		for (const elem of repeatArr.values()) {
			elem[1](res.data);
		}
		return Promise.reject(res.data);
	} else {
		for (const elem of repeatArr.values()) {
			elem[0](res.data);
		}
		return Promise.resolve(res.data);
	}
}
function responseError(error: AxiosError) {
	let { message } = error;
	if (message !== '取消请求') {
		if (message == 'Network Error') {
			message = '后端接口连接异常';
		} else if (message.includes('timeout')) {
			message = '系统接口请求超时';
		} else if (message.includes('Request failed with status code')) {
			message = '系统接口' + message.substr(message.length - 3) + '异常';
		}
		ElMessage({
			message: message,
			type: 'error',
			duration: 5 * 1000
		});
		loadingInstance?.close();
		cancelFn();
	}
	return Promise.reject(error);
}
//@ts-ignore
service.interceptors.response.use(responseSuccess, responseError);

/**
 * Axios 实例
 * @example
 *
 *  // 基础用法
 *  import axios from '@/utils/axios'
 *  axios({
 *    method: 'post',
 *    url: '/user/123',
 *    data: {
 *      firstName: 'Fred',
 *      lastName: 'Flintstone'
 *    }
 *  })
 *
 *  @example
 *
 *  // 实例方法
 *  axios.request(config)
 *  axios.get(url[, config])
 *  axios.delete(url[, config])
 *  axios.head(url[, config])
 *  axios.options(url[, config])
 *  axios.post(url[, data[, config]])
 *  axios.put(url[, data[, config]])
 *  axios.patch(url[, data[, config]])
 */

/**
 *
 * @param {object} options
 * 请求配置参数
 * url请求地址必须传
 * method请求方法默认为get方法
 * data请求参数
 */
export default function (options: AxiosRequestConfig & { urlParams?: objType }) {
	// 处理默认参数，传参和默认参数合并
	const config = Object.assign({ method: 'get' }, options || {});

	// 必须要传入url
	if (!config.url) {
		throw new Error('ajax url is required!');
	}

	let { url, data } = config;
	const { method } = config;
	// page接口默认添加needCount：1，需要总数
	if (config.url.split('/').pop() === 'page' || config.url.split('/').pop() === 'pageSenior') {
		data = Object.assign({ needCount: 1 }, data || {});
	}
	// export接口默认responseType = 'blob'
	if (config.url.split('/').pop() === 'export') {
		config.responseType = 'blob';
	}

	// 从缓存中提取已经解析过的url
	// url 支持参数信息，如： /api/path/:id
	// 这种情况需要把url解析成一个正则表达式，然后再跟参数匹配组成一个真正要请求的url
	if (config.url.includes(':')) {
		let compileCache = pathToRegexCaches.get(url);
		const host = getHost(url);
		if (!compileCache) {
			// 先排除host段，因为host段的端口号与参数写法有冲突
			compileCache = compile(url.replace(host, ''), {});
			pathToRegexCaches.set(url, compileCache);
		}
		// 出去传输过来的url参数，并补回host段
		url = host + compileCache(config.urlParams);
	}

	// 删除无需传递的字段
	['url', 'method', 'data', 'urlParams'].forEach((key) => {
		delete config[key as keyof typeof config];
	});

	// 判断是否重复
	const key = ['get', 'head', 'delete'].includes(method) ? createKey({ url, method, params: data }) : createKey({ url, method, data });

	if (httpPromiseArr.has(key)) {
		return new Promise((resolve, reject) => {
			httpPromiseArr.get(key).repeatArr.push([resolve, reject]);
		});
	} else {
		httpPromiseArr.set(key, { cancel: '', repeatArr: [] });

		return service({
			...config,
			url,
			method,
			[['get', 'head', 'delete'].includes(method) ? 'params' : 'data']: data
		});
	}
}
